Spring AOP 编程理解

一、AOP的理解

最近在学习Spring,理解涉及到Spring的两大核心部分,一个是IOC(控制反转),另一个就是AOP(面向切面编程),今天主要讲一下什么是面向切面编程?

在知乎上看到别人的回答,有句话觉得特别的有道理,“这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程”

到底什么是AOP呢?就是把逻辑代码和处理繁琐事务的代码分离开,以便能够分离复杂度,让人在同一时间只用思考代码逻辑,或者琐碎事务,代码逻辑比如插入一条数据,那么琐碎事务就包括获取连接和关闭连接,事务开始,事务提交。

一般而言,我们将切入到指定类指定方法的代码片称为切面,而切入到哪些类、哪些方法则叫切入点,有了AOP,就可以将几个类共有的代码抽取到一个切片中,等到需要的时候再切入到对象中取,从而改变原有的行为。

举个例子:

先假设你有一段逻辑代码要写,在这段代码之前要写log,代码完成之后要写log,结局就是一大堆的log淹没了逻辑代码,而AOP的思想就是将非逻辑部分的代码抽离出来,只考虑逻辑代码就行了。

和AOP对应的是OOP,对于OOP,其为从横向上区分出一个个的类,而AOP从纵向上向对象中加入特定的代码。

Spring实现的AOP是代理模式(后面会介绍一下代理模式),给调用者实际使用的是已经加工过的对象。

二、动态代理

假设有这样几个类:

ArithmeticCalculator.java

1
2
3
4
5
6
7
// ArithmeticCalculator接口
public interface ArithmeticCalculator {
int add(int i, int j);
int sub(int i, int j);
int mul(int i, int j);
int div(int i, int j);
}

其有一个实现类:

ArithmeticCalculatorImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ArithmeticCalculatorImpl implements ArithmeticCalculator {
public int add(int i, int j) {
int result = i + j;
return result;
}
public int sub(int i, int j) {
int result = i - j;
return result;
}
public int mul(int i, int j) {
int result = i * j;
return result;
}
public int div(int i, int j) {
int result = i / j;
return result;
}
}

若现在想实现这样的功能:在程序执行期间追踪正在发生的活动,此时代码将变为:

ArithmeticCalculatorLoggingImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class ArithmeticCalculatorLoggingImpl implements ArithmeticCalculator {
public int add(int i, int j) {
System.out.println("The method add begins with[" + i + "," + j + "]");
int result = i + j;
System.out.println("The method add ends with " + result);
return result;
}
public int sub(int i, int j) {
System.out.println("The method sub begins with[" + i + "," + j + "]");
int result = i - j;
System.out.println("The method sub ends with " + result);
return result;
}
public int mul(int i, int j) {
System.out.println("The method mul begins with[" + i + "," + j + "]");
int result = i * j;
System.out.println("The method mul ends with " + result);
return result;
}
public int div(int i, int j) {
System.out.println("The method div begins with[" + i + "," + j + "]");
int result = i / j;
System.out.println("The method div ends with " + result);
return result;
}
}

这样大量的log就淹没了业务逻辑代码,并且代码存在大量的重复,且如果要修改日志,此必须对每一处进行修改,显然这不是一种好的解决方法。

这时候动态代理就可以粉墨登场了!!!

ArithmeticCalculatorLoggingProxy.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class ArithmeticCalculatorLoggingProxy {
// 要代理的对象
private ArithmeticCalculator target;
public ArithmeticCalculatorLoggingProxy(ArithmeticCalculator target) {
this.target = target;
}
// 获取代码对象
public ArithmeticCalculator getLoggingProxy() {
ArithmeticCalculator proxy = null;
// 因为此时的代理对象proxy并不是像普通的对象一样是通过new得到的,此时java虚拟机有默认的类加载器,而现在这个对象是通过Proxy.newProxyInstance()方法得到的,因此必须告诉java虚拟机这个代理对象是由哪个类加载期进行加载
ClassLoader loader = target.getClass().getClassLoader();
// 实际的代理类使用的哪个接口,即代理类是什么类型
Class[] interfaces = new Class[]{ArithmeticCalculator.class};
// 当调用代理对象其中的方法时,该执行的代码
InvocationHandler h = new InvocationHandler() {
/**
* proxy: 正在返回的那个代理对象,一般情况下,在invoke方法中不使用该对象
* method:正在被调用的方法
* args: 调用方法时传入的参数
*/
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
String methodName = method.getName();
System.out.println("The method " + methodName + "begins with " + Arrays.asList(args));
Object result = method.invoke(target, args);
System.out.println("The method " + methodName + "ends with " + result);
return result;
}
};
proxy = (ArithmeticCalculator) Proxy.newProxyInstance(loader, interfaces, h);
return proxy;
}
}

而此时的main函数如下:

1
2
3
4
5
6
7
8
9
10
11
public class Main {
public static void main(String[] args) {
ArithmeticCalculator target = new ArithmeticCalculatorImpl();
ArithmeticCalculator proxy = new ArithmeticCalculatorLoggingProxy(target).getLoggingProxy();
int result = proxy.add(1, 2);
System.out.println("-->" + result);
result = proxy.div(4, 2);
System.out.println("-->" + result);
}
}

此时,ArithmeticCalculatorImpl中的函数全部都是业务逻辑,而与业务逻辑无关的代码均在代理中,若想修改业务逻辑无关代码,只需要在ArithmeticCalculatorLoggingProxy中进行修改即可,而无需修改ArithmeticCalculatorImpl中的代码!